#include "shmhdl.hpp"

#include <atomic>
#include <mutex>
#include <system_error>

namespace shm_kernel::shared_memory {

shm_handle::shm_handle(std::string name, const shmsz_t& nbytes)
{
  // calculate required bytes
  ULARGE_INTEGER __nbytes;
  size_t         __reqbytes = nbytes + sizeof(shm_meta_t);
  CopyMemory(&__nbytes, &__reqbytes, sizeof(uint64_t));
  HANDLE __hMapFile = CreateFileMappingA(INVALID_HANDLE_VALUE,
                                         nullptr,
                                         PAGE_READWRITE,
                                         __nbytes.HighPart,
                                         __nbytes.LowPart,
                                         name.c_str());
  // Win32 API, even the name is used by existing shared memory object,
  // CreateFileMapping with the exact same name will still return a valid
  // HANDLE... This is wired.
  if (GetLastError() == ERROR_ALREADY_EXISTS) {
    CloseHandle(__hMapFile);
    std::error_code ec(ERROR_ALREADY_EXISTS, std::system_category());
    char            errmsg[256];
    sprintf_s(errmsg,
              256,
              "fail to create shared memory object(%d). [%s]",
              ERROR_ALREADY_EXISTS,
              ec.message().c_str());
    throw std::runtime_error(errmsg);
  }
  // action failed
  if (__hMapFile == nullptr) {
    std::error_code ec(GetLastError(), std::system_category());
    char            errmsg[256];
    sprintf_s(errmsg,
              256,
              "fail to create shared memory object(%d). [%s]",
              ec.value(),
              ec.message().c_str());
    throw std::runtime_error(errmsg);
  }

  // setup shm object
  char* __meta = (char*)MapViewOfFile(
    __hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(shm_meta_t));
  if (__meta == nullptr) {
    std::error_code ec(GetLastError(), std::system_category());
    char            errmsg[256];
    sprintf_s(errmsg,
              256,
              "fail to map shared memory object(%d). [%s]",
              ec.value(),
              ec.message().c_str());
    CloseHandle(__hMapFile);
    throw std::runtime_error(errmsg);
  }
  this->meta_              = new (__meta) shm_meta_t;
  this->meta_->ref_count_  = 1;
  this->meta_->shm_status_ = SHM_STATUS::OK;
  this->meta_->shmsz_      = __reqbytes;

  // ! legacy code
  // this->ref_count_ = new (__meta) size_t(1);
  // this->shm_size_  = new (__meta + sizeof(size_t)) shmsz_t(__reqbytes);
  // this->mtx_ = new (__meta + sizeof(size_t) + sizeof(shmsz_t)) std::mutex();

  this->hMapFile_ = __hMapFile;
  this->name_     = std::move(name);

  this->addr_ = nullptr;
}

shm_handle::shm_handle(std::string name)
{
  // try to open shared memory object
  HANDLE __hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, true, name.c_str());
  if (__hMapFile == nullptr) {
    std::error_code ec(GetLastError(), std::system_category());
    char            errmsg[256];
    sprintf_s(errmsg,
              256,
              "fail to open shared memory object(%d). [%s]",
              ec.value(),
              ec.message().c_str());
    throw std::runtime_error(errmsg);
  }

  // map shm meta
  char* __meta = (char*)MapViewOfFile(
    __hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(shm_meta_t));
  if (__meta == nullptr) {
    std::error_code ec(GetLastError(), std::system_category());
    char            errmsg[256];
    sprintf_s(errmsg,
              256,
              "fail to map shared memory object(%d). [%s]",
              ec.value(),
              ec.message().c_str());
    CloseHandle(__hMapFile);
    throw std::runtime_error(errmsg);
  }
  shm_handle::shm_handle(shm_handle && handle) noexcept
  {
    this->meta_     = handle.meta_;
    this->hMapFile_ = handle.hMapFile_;
    this->name_     = std::move(handle.name_);
  }

  // setup shm meta
  this->meta_ = reinterpret_cast<shm_meta_t*>(__meta);
  this->meta_->mtx_.lock();
  // check shm_meta status
  if (this->meta_->shm_status_ == SHM_STATUS::DEL) {
    this->meta_->mtx_.unlock();
    UnmapViewOfFile(__meta);
    CloseHandle(__hMapFile);
    throw std::runtime_error("shm object is already unlinked.");
  }
  this->meta_->ref_count_ += 1;
  this->meta_->mtx_.unlock();

  // setup local var
  this->hMapFile_ = __hMapFile;
  this->name_     = std::move(name);
  this->addr_     = nullptr;
}

shm_handle::~shm_handle()
{
  // hMapFile_ == nullptr means unlink is already called.
  if (hMapFile_ != nullptr) {
    this->meta_->mtx_.lock();
    this->meta_->ref_count_ -= 1;
    if (this->meta_->ref_count_ == 0) {
      this->meta_->shm_status_ = SHM_STATUS::DEL;
    }
    this->meta_->mtx_.unlock();
    this->unmap();
    this->unmap_meta();
    CloseHandle(hMapFile_);
  }
  if (this->addr_ != nullptr) {
    this->unmap();
  }
  if (this->meta_ != nullptr) {
    this->unmap_meta();
  }
}

void
shm_handle::update_status() noexcept
{
  // doing nothing for win32 API
  return;
}

void
shm_handle::update_status(std::error_code& ec) noexcept
{
  // doing nothing for win32 API
  ec.clear();
  return;
}

void*
shm_handle::map() noexcept
{
  if (this->addr_ == nullptr) {
    char* __ptr;
    __ptr = reinterpret_cast<char*>(
      MapViewOfFile(hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, 0));
    if (__ptr == nullptr) {
      return nullptr;
    }
    this->addr_ = __ptr + sizeof(shm_meta_t);
    return this->addr_;
  }
  // if already mapped, just return addr_
  return this->addr_;
}

void*
shm_handle::map(void* addr) noexcept
{
  // currently not supported for win32 API
  SetLastError(ERROR_BAD_ENVIRONMENT);
  return nullptr;

  if (addr == nullptr) {
    return this->map();
  }
  if (this->addr_ == nullptr) {
    this->addr_ = MapViewOfFileEx(
      hMapFile_, FILE_MAP_ALL_ACCESS, 0, sizeof(shm_meta_t), 0, addr);
    return this->addr_;
  }
  // when this->addr_ != addr
  if (this->addr_ != addr) {
    UnmapViewOfFile(this->addr_);
    this->addr_ = MapViewOfFileEx(
      hMapFile_, FILE_MAP_ALL_ACCESS, 0, sizeof(shm_meta_t), 0, addr);
    return this->addr_;
  }
  // when this->addr_ == addr
  return this->addr_;
}

void*
shm_handle::map(std::error_code& ec) noexcept
{
  ec.clear();
  char* __ptr;
  if (this->addr_ == nullptr) {
    __ptr = reinterpret_cast<char*>(
      MapViewOfFile(hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, 0));
    if (__ptr == nullptr) {
      ec.assign(GetLastError(), std::system_category());
      return nullptr;
    }
    this->addr_ = __ptr + sizeof(shm_meta_t);
    return this->addr_;
  }

  // if already mapped
  return this->addr_;
}

void*
shm_handle::map(void* addr, std::error_code& ec) noexcept
{
  ec.assign(ERROR_BAD_ENVIRONMENT, std::system_category());
  return nullptr;

  ec.clear();
  if (addr == nullptr) {
    return this->map(ec);
  }
  if (this->addr_ == nullptr) {
    this->addr_ = MapViewOfFileEx(
      hMapFile_, FILE_MAP_ALL_ACCESS, 0, sizeof(shm_meta_t), 0, addr);
    if (this->addr_ == nullptr) {
      ec.assign(GetLastError(), std::system_category());
      return nullptr;
    }
    return this->addr_;
  }
  if (this->addr_ == addr) {
    return this->addr_;
  }
  // when addr != this->addr_
  UnmapViewOfFile(this->addr_);
  this->addr_ = MapViewOfFileEx(
    hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(shm_meta_t), addr);
  if (this->addr_ == nullptr) {
    ec.assign(GetLastError(), std::system_category());
    return nullptr;
  }
  return this->addr_;
}

void
shm_handle::unmap_meta() noexcept
{
  if (this->meta_ != nullptr) {
    UnmapViewOfFile(this->meta_);
    this->meta_ = nullptr;
  }
}

void
shm_handle::unmap_meta(std::error_code& ec) noexcept
{
  if (this->meta_) {
    if (UnmapViewOfFile(this->meta_) == 0) {
      ec.assign(GetLastError(), std::system_category());
      return;
    }
    this->meta_ = nullptr;
  }
}

void
shm_handle::unmap() noexcept
{
  if (this->addr_) {
    UnmapViewOfFile(reinterpret_cast<char*>(this->addr_) - sizeof(shm_meta_t));
    this->addr_ = nullptr;
  }
}

void
shm_handle::unmap(std::error_code& ec) noexcept
{
  if (this->addr_) {
    if (UnmapViewOfFile(reinterpret_cast<char*>(this->addr_) -
                        sizeof(shm_meta_t)) == 0) {
      ec.assign(GetLastError(), std::system_category());
      return;
    }
    this->addr_ = nullptr;
  }
}

void
shm_handle::unlink() noexcept
{
  // doing nothing for win32 API
  this->meta_->mtx_.lock();
  this->meta_->shm_status_ = SHM_STATUS::DEL;
  this->meta_->mtx_.unlock();
}

void
shm_handle::unlink(std::error_code& ec) noexcept
{
  // doing nothing for win32 API
  ec.clear();
  this->meta_->mtx_.lock();
  this->meta_->shm_status_ = SHM_STATUS::DEL;
  this->meta_->mtx_.unlock();
}

const shmsz_t
shm_handle::nbytes() noexcept
{
  return this->meta_->shmsz_;
}

std::string_view
shm_handle::name() noexcept
{
  return this->name_;
}

void*
shm_handle::addr() noexcept
{
  return this->addr_;
}

const size_t&
shm_handle::ref_count() noexcept
{
  return this->meta_->ref_count_;
}

HANDLE
shm_handle::native_handle() noexcept
{
  return this->hMapFile_;
}

}
